/*
	Copyright (C) 2011 Salil Bhagurkar

	This file is part of illusion

	illusion is free software: you can redistribute it and/or modify
	it under the terms of the GNU Lesser General Public License as
	published by the Free Software Foundation, either version 3 of
	the License, or (at your option) any later version.

	illusion is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU Lesser General Public License for more details.

	You should have received a copy of the GNU Lesser General Public
	License along with illusion. 
	If not, see <http://www.gnu.org/licenses/>.
*/

//PC Keyboard basic driver

#include <arch/arch.h>
#include <arch/portio.h>
#include <arch/power.h>
#include <klib/lib.h>
#include <klib/string.h>
#include <klib/memory.h>
#include <kernel/errors.h>
#include <kernel/input.h>
#include <kernel/console.h>
#include <kernel/irq.h>


static void clear_keys()
{
	inb(0x60);
	inb(0x60);
	inb(0x60);
	inb(0x60);
}



static uint_t base_keymap[] = {
		0, KEY_ESC, '1', '2', '3', '4', '5', '6', '7', '8', '9', '0',
		'-', '=', KEY_BACKSPACE, KEY_TAB, 'q', 'w', 'e', 'r', 't', 'y',
		'u', 'i', 'o', 'p', '[', ']', KEY_ENTER, KEY_LCTRL,
		'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', '\'', '`',
		KEY_LSHIFT, '\\', 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.',
		'/', KEY_RSHIFT, '*', KEY_LALT, KEY_SPACE, KEY_CAPS_LK,
		KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5, KEY_F6, KEY_F7, KEY_F8,
		KEY_F9, KEY_F10, KEY_NUM_LK, KEY_SCROLL_LK,
/*Numpad*/	KEY_HOME, KEY_UP, 0, '-', KEY_LEFT, 0, KEY_RIGHT, '+', KEY_END, KEY_DOWN, 0, 0, KEY_DELETE,
		KEY_SYSRQ, 0, 0, KEY_F11, KEY_F12};

static uint_t num_keymap[] = {
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, '*', 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0,
/*Numpad*/	'7', '8', '9', '-', '4', '5', '6', '+', '1', '2', '3', '0', '.',
		0, 0, 0, 0, 0};

static uint_t shift_keymap[] = {
		0, 0, '!', '@', '#', '$', '%', '^', '&', '*', '(', ')',
		'_', '+', 0, 0, 'Q', 'W', 'E', 'R', 'T', 'Y',
		'U', 'I', 'O', 'P', '{', '}', 0, 0,
		'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ':', '"', '~',
		0, '|', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '<', '>',
		'?', 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0,
/*Numpad*/	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0};

static uint_t caps_keymap[] = {
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 'Q', 'W', 'E', 'R', 'T', 'Y',
		'U', 'I', 'O', 'P', 0, 0, 0, 0,
		'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 0, 0, 0,
		0, 0, 'Z', 'X', 'C', 'V', 'B', 'N', 'M', 0, 0,
		0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0,
/*Numpad*/	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0};

static uint_t extended_keymap[] = {
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, KEY_RCTRL, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, '/', 0, KEY_PRNT_SCRN,
		KEY_RALT, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		KEY_HOME, KEY_UP, KEY_PGUP, 0, KEY_LEFT, 0, KEY_RIGHT, 0, KEY_END,
		KEY_DOWN, KEY_PGDOWN, KEY_INSERT, KEY_DELETE, 0, 0, 0, 0, 0};

#define MAP_LEN (sizeof(base_keymap) / sizeof(base_keymap[0]))



static struct input_driver pckbd_driver;

static bool extended = false;

#define KBD_STATUS_SHIFT (1 << 0)
#define KBD_STATUS_CAPS (1 << 1)
#define KBD_STATUS_NUM (1 << 2)
#define KBD_STATUS_SCROLL (1 << 3)


static void wait_kbc()
{
	while((inb(0x64) & 2) != 0);
}

static u32 kbd_status;

#define KBD_LED_SCROLL 1
#define KBD_LED_NUM 2
#define KBD_LED_CAPS 4
static void update_leds()
{
	u32 hw_status = 0;
	if(kbd_status & KBD_STATUS_CAPS) {
		hw_status |= KBD_LED_CAPS;
	}
	if(kbd_status & KBD_STATUS_NUM) {
		hw_status |= KBD_LED_NUM;
	}
	if(kbd_status & KBD_STATUS_SCROLL) {
		hw_status |= KBD_LED_SCROLL;
	}
	wait_kbc();
	outb(0x60, 0xed);

	wait_kbc();
	outb(0x60, hw_status);
}

static void toggle_bit(uint_t bit)
{
	if(kbd_status & bit) kbd_status &= ~bit;
	else kbd_status |= bit;
	update_leds();
}

static int update_status(uint_t key, char key_state)
{
	if(key == KEY_LSHIFT || key == KEY_RSHIFT) {
		if(key_state == KEYSTATE_DOWN)
			kbd_status |= KBD_STATUS_SHIFT;
		else
			kbd_status &= ~KBD_STATUS_SHIFT;
		return 1;
	} else if(key == KEY_CAPS_LK) {
		if(key_state == KEYSTATE_DOWN) {
			toggle_bit(KBD_STATUS_CAPS);
		}
		return 1;
	} else if(key == KEY_NUM_LK) {
		if(key_state == KEYSTATE_DOWN)
			toggle_bit(KBD_STATUS_NUM);
		return 1;
	} else if(key == KEY_SCROLL_LK) {
		if(key_state == KEYSTATE_DOWN)
			toggle_bit(KBD_STATUS_SCROLL);
		return 1;
	}
	return 0;
}

static void pass_key(uint_t key, uint_t state)
{
	pckbd_driver.received_key(&pckbd_driver, key, state);
}

static void mod_key(uint_t x, uint_t state)
{
	uint_t key;
	key = base_keymap[x];
	if(kbd_status & KBD_STATUS_NUM) {
		key = num_keymap[x];
		if(key > 0) {
			pass_key(key, state);
			return;
		}
	}
	if(kbd_status & KBD_STATUS_CAPS) {
		if(kbd_status & KBD_STATUS_SHIFT) {
			key = caps_keymap[x];
			if(key > 0) {
				//Revert because of shift
				key = base_keymap[x];
				pass_key(key, state);
			} else {
				key = shift_keymap[x];
				pass_key(key, state);
			}
		} else {
			key = caps_keymap[x];
			if(key > 0) {
				pass_key(key, state);
			} else {
				key = base_keymap[x];
				pass_key(key, state);
			}
		}
	} else {
		if(kbd_status & KBD_STATUS_SHIFT) {
			key = shift_keymap[x];
			if(key > 0) {
				pass_key(key, state);
			} else {
				key = base_keymap[x];
				pass_key(key, state);
			}
		} else {
			key = base_keymap[x];
			pass_key(key, state);
		}
	}
}

static int keyb_irq()
{
	u8 x;
	uint_t key;
	x = inb(0x60);
	if(x == 0xe0) {
		extended = true;
	} else {
		if(!(x & 0x80)) {
			//Key down
			if(extended) {
				pass_key(extended_keymap[x], KEYSTATE_DOWN);
			} else {
				unsigned long key = base_keymap[x];
				if(update_status(key, KEYSTATE_DOWN)) {
					pass_key(key, KEYSTATE_DOWN);
					return 0;
				}
				mod_key(x, KEYSTATE_DOWN);
			}
		} else {
			x &= 0x7f;
			//Key release
			if(extended) {
				pass_key(extended_keymap[x], KEYSTATE_UP);
			} else {
				unsigned long key = base_keymap[x];
				if(update_status(key, KEYSTATE_UP)) {
					pass_key(key, KEYSTATE_UP);
					return 0;
				}
				mod_key(x, KEYSTATE_UP);
			}
		}
		extended = false;
	}
	return 0;
}

int pckbd_init()
{
	strcpy(pckbd_driver.name, "pc_keyboard", INPUT_DRIVER_NAME_LEN);
	input_register_driver(&pckbd_driver);
	irq_set_handler(1, keyb_irq);
	clear_keys();
	return 0;
}
